/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import { Reader } from '../io/Reader';
import { Document } from '../nodes/Document';
import { Element } from '../nodes/Element';
import { CharacterReader } from './CharacterReader';
import { Parser } from './Parser';
import { ParseSettings } from './ParseSettings';
import { Token} from './Token';
import { Tokeniser } from './Tokeniser';
import ArrayList from '@ohos.util.ArrayList';
import List from '@ohos.util.List';
import { Node } from '../nodes/Node';
import { Attributes } from '../nodes/Attributes';
import { ParseError } from './ParseError';
import { Tag } from './Tag';
import { Validate } from '../helper/Validate';
import HashMap from '@ohos.util.HashMap';
import wantAgent from '@ohos.wantAgent';
import { Range } from '../nodes/Range';
import { TokenStartTag } from './token/TokenStartTag';
import { TokenEndTag } from './token/TokenEndTag';
import { TokenType } from './TokenType';
import { Position } from '../nodes/Position';
import hilog from '@ohos.hilog';

export abstract class TreeBuilder {
  parser: Parser
  reader: CharacterReader|null
  tokeniser: Tokeniser|null
  //current doc we are building into
  protected doc: Document
  // the stack of open elements
  protected stack: List<Element>|null
  // current base uri, for creating new elements
  protected baseUri: string
  // currentToken is used only for error tracking.
  protected currentToken: Token;
  settings: ParseSettings
  // tags we've used in this parse; saves tag GC for custom tags.
  seenTags:HashMap<string,Tag> | null
  // start tag to process
  private _start = new TokenStartTag()
  private _end = new TokenEndTag()

  abstract defaultSettings(): ParseSettings;
  // optionally tracks the source range of nodes
  private _trackSourceRange:boolean

  protected initialiseParse(input: Reader, baseUri: string, parser: Parser) {
    Validate.notNullParam(input, "input")
    Validate.notNullParam(baseUri, "baseUri")
    Validate.notNull(parser)
    this.doc = new Document(baseUri)
    this.doc.setParser(parser)
    this.parser = parser
    this.settings = parser.getSettings()
    this.reader = new CharacterReader(input)
    this._trackSourceRange = parser.isTrackPosition()
    // when tracking errors or source ranges, enable newline tracking for better legibility
    this.reader.trackNewlines(parser.isTrackPosition() || this._trackSourceRange)
    this.currentToken = null;
    this.tokeniser = new Tokeniser(this.reader, parser.getErrors())
    this.stack = new List();
    this.seenTags = new HashMap()
    this.baseUri = baseUri;
  }

  parse(input: Reader, baseUri: string, parser: Parser): Document {
    this.initialiseParse(input, baseUri, parser)

    this.runParser()
    // tidy up - as the Parser and Treebuilder are retained in document for settings / fragments
    this.reader.close();
    this.reader = null;
    this.tokeniser = null;
    this.stack = null;
    this.seenTags = null
    return this.doc;
  }

  abstract newInstance():TreeBuilder
  abstract parseFragment(inputFragment: string, context: any, baseUri: string, parser: any): List<Node>;

  protected runParser() {
    let tokeniser = this.tokeniser
    let eof = TokenType.EOF
    while (true) {
      let token = tokeniser.read()
      this.process(token)
      token.reset()
      if (token.type === eof) {
        break
      }
    }
  }

  protected abstract process(token: Token): boolean

  processStartTag(name: string,attrs: Attributes|null = null): boolean {
    // these are "virtual" start tags (auto-created by the treebuilder), so not tracking the start position
    let start = this._start;
    if (attrs === undefined || attrs === null) {
      // don't recycle an in-use token
      if (this.currentToken === start) {
        return this.process(new TokenStartTag().setName(name));
      }
      return this.process(start.reset().setName(name));
    } else {
      // don't recycle an in-use token
      if (this.currentToken == start) {
        return this.process(new TokenStartTag().nameAttr(name, attrs));
      }
      start.reset();
      start.nameAttr(name, attrs);
      return this.process(start);
    }

  }

  processEndTag(name: string) {
    // don't recycle an in-use token
    if (this.currentToken == this._end) {
      return this.process(new TokenEndTag().setName(name));
    }
    return this.process(this._end.reset().setName(name));
  }

  /**
   Get the current element (last on the stack). If all items have been removed, returns the document instead
   (which might not actually be on the stack; use stack.size() == 0 to test if required.
   @return the last element on the stack, if any; or the root document
   */
  currentElement(): Element {
    let size = this.stack.length;
    if (size > 0) {
      return this.stack.get(size - 1)
    } else {
      return this.doc
    }
  }

  /**
   Checks if the Current Element's normal name equals the supplied name.
   @param normalName name to check
   @return true if there is a current element on the stack, and its name equals the supplied
   */
  currentElementIs(normalName:string):boolean {
    if (this.stack.length === 0) {
      return false
    }
    let current = this.currentElement()
    return current!== undefined && current !== null && current.normalName() === normalName
  }

  protected error(msg: string) {
    let errors = this.parser.getErrors();
    if (errors.canAddError())
      errors.add(new ParseError(this.reader, msg));
  }

  /**
   (An internal method, visible for Element. For HTML parse, signals that script and style text should be treated as
   Data Nodes).
   */
  isContentForTagData(normalName:string):boolean {
    return false
  }

  tagFor(tagName:string, settings:ParseSettings):Tag {
    // note that we don't normalize the cache key. But tag via valueOf may be normalized.
    let tag = this.seenTags.get(tagName)
    if (tag === undefined || tag === null) {
      Validate.notEmpty(tagName,'TreeBuilder tagFor tagName  must not be empty')
      tag = Tag.valueOf(tagName, settings)
      this.seenTags.set(tagName, tag)
    }
    return tag
  }

  /**
   Called by implementing TreeBuilders when a node has been inserted. This implementation includes optionally tracking
   the source range of the node.
   * @param node the node that was just inserted
   * @param token the (optional) token that created this node
   */
  onNodeInserted(node:Node, token:Token) {
    this.trackNodePosition(node, token, true)
  }

  /**
   Called by implementing TreeBuilders when a node is explicitly closed. This implementation includes optionally
   tracking the closing source range of the node.
   * @param node the node being closed
   * @param token the end-tag token that closed this node
   */
  protected onNodeClosed(node:Node, token:Token) {
    this.trackNodePosition(node, token, false)
  }

  private trackNodePosition(node:Node, token:Token,start:boolean) {
    if (this._trackSourceRange && token !== undefined && token !== null) {
      let startPos = token.getStartPos()
      if (startPos === Token.Unset) {
        return
      }
      let startRange = new Position(startPos, this.reader.lineNumber1(startPos), this.reader.columnNumber1(startPos))
      let endPos = token.getEndPos()
      let endRange = new Position(endPos, this.reader.lineNumber1(endPos), this.reader.columnNumber1(endPos))
      let range = new Range(startRange, endRange)
      range.track(node, start)
    }
  }

}
